using System;
using System.Collections.Generic;
using System.Text;
using CodePlex.Resourceful;
using CodePlex.Resourceful.Amazon.S3;
using System.Xml;
using System.IO;
using System.Reflection;

namespace CodePlex.SpaceBlock.FS
{
    public class NirvanixFileSystem : BaseFileSystem, IFileSystem, ISupportsTransferWithMetadata
    {
        private readonly NirvanixClient _client;
        private readonly Tracer _tracer;

        private readonly string _appKey;
        private readonly string _appName;
        private readonly string _username;
        private readonly string _password;

        private bool _isValidated;
        private string _sessionToken;


        public NirvanixFileSystem(string appKey, string appName, string username, string password, bool useSSL)
        {
            _tracer = new Tracer();

            _client = new NirvanixClient(useSSL);
            _client.Tracer.AddListener(this.Tracer);

            _appKey = appKey;
            _appName = appName;
            _username = username;
            _password = password;

        }


        // misc

        public Tracer Tracer
        {
            get { return _tracer; }
        }

        public string Caption
        {
            get
            {
                return string.Format("Nirvanix File System - {0}", this.ShortCaption); 
            }
        }

        public string ShortCaption
        {
            get
            {
                if (!_isValidated)
                {
                    // side-effect alert!
                    this.Validate();
                    _isValidated = true;
                }

                return _username + (_client.IsSSL ? " (ssl)" : "");
            }
        }




        // acl

        //public ACL GetACL(Path path)
        //{
        //    throw new NotImplementedException();
        //}

        //public void SetACL(Path path, ACL acl)
        //{
        //    throw new NotImplementedException();
        //}

        //public ReadOnlyList<ACLPrincipal> GetPrincipals()
        //{
        //    throw new NotImplementedException();
        //}

        //public ReadOnlyList<string> GetPermissions()
        //{
        //    throw new NotImplementedException();
        //}















        // path manip - non-destructive

        public bool Exists(Path path)
        {
            this.CheckNull(path);

            if (path.IsFolder)
            {
                ClientResponse r = _client.IMFS_ListFolder(GetSessionToken(), path.ToString(), 1, 0, null, null, null, null);
                //<Response>
                //    <ResponseCode>70005</ResponseCode>
                //    <ErrorMessage>Invalid path</ErrorMessage>
                //</Response>

                //<Response>
                //    <ResponseCode>0</ResponseCode>
                //    <ListFolder>
                //        <TotalFolderCount>0</TotalFolderCount>
                //        <TotalFileCount>0</TotalFileCount>
                //        <PageFolderCount>0</PageFolderCount>
                //        <PageFileCount>0</PageFileCount>
                //    </ListFolder>
                //</Response>

                return r.AsXmlDocument().SelectSingleNode("//ResponseCode").InnerText == "0";
            }
            else
            {
                ClientResponse r = _client.IMFS_GetDownloadNodes(GetSessionToken(), path.ToString());


                return r.AsXmlDocument().SelectSingleNode("//ResponseCode").InnerText == "0";

            }
        }




        public ReadOnlyList<IPathItem> GetItems(Path path)
        {
            this.CheckNull(path);
            this.CheckFolder(path);

            return new ReadOnlyList<IPathItem>(F.Inscrutable<XmlElement, IPathItem, Path>(path, this.ListFolder, BuildFolder, BuildFile));
        }

        public ReadOnlyList<IFolderItem> GetFolders(Path path)
        {
            this.CheckNull(path);
            this.CheckFolder(path);

            return new ReadOnlyList<IFolderItem>(F.InscrutableDowncast<XmlElement, IPathItem, Path,IFolderItem>(path, this.ListFolder, BuildFolder));
        }

        public ReadOnlyList<IFileItem> GetFiles(Path path)
        {
            this.CheckNull(path);
            this.CheckFolder(path);

            return new ReadOnlyList<IFileItem>(F.InscrutableDowncast<XmlElement, IPathItem, Path,IFileItem>(path, this.ListFolder, BuildFile));
        }


        public IPathItem GetItem(Path path)
        {
            this.CheckNull(path);

            if (!this.Exists(path))
            {
                return null;
            }

            return this.GetItems(path.Parent).Find(delegate(IPathItem item)
            {
                return item.Path.Equals(path);
            });
        }




        // path manip - destructive

        public void EnsureFolderExists(Path path)
        {
            this.CheckNull(path);
            this.CheckFolder(path);

            string folderPath = path.ToString();
            _client.IMFS_CreateFolders(GetSessionToken(), folderPath);
        }


        public void EnsureFileExists(Path path)
        {

            this.CheckNull(path);
            this.CheckFile(path);


            string contents = " ";   // nirvanix does not accept 0-length files

            this.Upload(path, contents,null);
        }



        public void Delete(Path path)
        {
            this.CheckNull(path);

            if (path.IsFolder)
            {
                _client.IMFS_DeleteFolders(GetSessionToken(), path.ToString());
            }
            else
            {
                _client.IMFS_DeleteFiles(GetSessionToken(), path.ToString());
            }
        }







        // metadata




        private static class NirvanixMD
        {
            public static readonly string AppKey = "AppKey";
            public static readonly string AppName = "AppName";
            public static readonly string Username = "Username";
            public static readonly string SessionToken = "SessionToken";

            public static readonly string AcceptRanges = HttpHeader.AcceptRanges;
            public static readonly string CacheControl = HttpHeader.CacheControl;
            public static readonly string ContentLength = HttpHeader.ContentLength;
            public static readonly string ContentMD5 = HttpHeader.ContentMD5;
            public static readonly string ContentTransferEncoding = "Content-Transfer-Encoding";
            public static readonly string Date = HttpHeader.Date;
            public static readonly string ETag = HttpHeader.ETag;
            public static readonly string LastModified = HttpHeader.LastModified;
            public static readonly string Server = "Server";
            public static readonly string Vary = HttpHeader.Vary;
            public static readonly string XAspNetVersion = "X-AspNet-Version";
            public static readonly string XPoweredBy = "X-Powered-By";

            public static readonly string MD5 = "MD5";

        }

        override protected string GetMetadataCategory(string key)
        {

            if (Utils.In(key,

                NirvanixMD.AppKey,
                NirvanixMD.AppName,
                NirvanixMD.Username,
                NirvanixMD.SessionToken

                ))
            {
                return "Nirvanix Authentication";
            }

            if (Utils.In(key,

                NirvanixMD.AcceptRanges,
                NirvanixMD.CacheControl,
                NirvanixMD.ContentLength,
                NirvanixMD.ContentMD5,
                NirvanixMD.ContentTransferEncoding,
                NirvanixMD.Date,
                NirvanixMD.ETag,
                NirvanixMD.LastModified,
                NirvanixMD.Server,
                NirvanixMD.Vary,
                NirvanixMD.XAspNetVersion,
                NirvanixMD.XPoweredBy

                ))
            {
                return "Nirvanix HTTP";
            }


            string baseCategory = base.GetMetadataCategory(key);

            if (baseCategory != "Other")
            {
                return baseCategory;
            }

            return "Nirvanix Metadata";


        }

        override protected string GetMetadataDescription(string key, string category)
        {

            NameValuePairs descriptions = new NameValuePairs();

            descriptions[NirvanixMD.AppKey] = "If you call the login method with a master account you must also provide an application key so it can be determined which application you wish to do file operations on.";
            descriptions[NirvanixMD.AppName] = "To download a file from the Nirvanix MFS, you can make a REST request to the transfer node using the GET method. The URL has the following format: http://<Node>/<sessionToken>/<appName>/<userName>/<path>";
            descriptions[NirvanixMD.Username] = "The authentication system takes in the application key and username/password to authenticate.";
            descriptions[NirvanixMD.SessionToken] = "The authentication system takes in the application key and username/password to authenticate. It returns a session token that will be used for subsequent calls into the system.";

            // this is actually a mime header:  http://tools.ietf.org/html/rfc1341
            descriptions[NirvanixMD.ContentTransferEncoding] = "A Content-Transfer-Encoding header field, which can be used to specify an auxiliary encoding that was applied to the data in order to allow it to pass through mail transport mechanisms which may have data or character set limitations.";

            descriptions[NirvanixMD.XAspNetVersion] = "Version of ASP.NET running on the server.";
            descriptions[NirvanixMD.XPoweredBy] = "Server-side technology responsible for servicing the request.";

            descriptions[NirvanixMD.MD5] = "Non-updatable custom metadata header calculated by Nirvanix.";


           
            string baseDescription =  descriptions.GetIfExists(key) ?? base.GetMetadataDescription(key, category);

            if (baseDescription != null)
            {
                return baseDescription;
            }

            return "Each file can have additional information stored about that file that we store as metadata. This metadata includes height and width for image files, and other secondary information about files that can be retrieved from the file contents.";
        }



        public Metadata GetMetadata(Path path)
        {

            Metadata rt = new Metadata();

            this.AddMetadata(rt, BaseMetadataKeys.Path, path.ToString());

            this.AddMetadata(rt, NirvanixMD.AppKey, _appKey);
            this.AddMetadata(rt, NirvanixMD.AppName, _appName);
            this.AddMetadata(rt, NirvanixMD.Username, _username);
            this.AddMetadata(rt, NirvanixMD.SessionToken, GetSessionToken());

            if (path.Level == 0)
            {
                this.AddMetadata(rt, BaseMetadataKeys.Type, "File System");

                return rt;
            }


            if (path.IsFolder)
            {
                this.AddMetadata(rt, BaseMetadataKeys.Type, "Virtual Folder");

                return rt;
            }




            // NEW (getpathinfo)

            //<?xml version="1.0" encoding="utf-8"?>
            //<Response>
            //   <ResponseCode>0</ResponseCode>
            //   <GetPathInfo>
            //      <ItemName>F8.jpg</ItemName>
            //      <IsFile>true</IsFile>
            //      <CreatedDate>Wed, 02 May 2007 13:36:41 GMT</CreatedDate>
            //      <SizeBytes>1000000</SizeBytes>
            //      <FileType>Image</FileType>
            //      <Metadata>
            //        <MetaData FileName="F8.jpg">
            //              <Data Key="Height">480</Data>
            //          <Data Key="MD5">JCiPH3sH4S3f/Ad93uWk+w==</Data>
            //              <Data Key="Width">640</Data>
            //        </MetaData>
            //      </Metadata>
            //      <Tags></Tags>
            //      <IsShared>false</IsShared>
            //      <ModifiedDate>Wed, 02 May 2007 13:36:41 GMT</ModifiedDate>
            //      <ItemID>1045632</ItemID>
            //   </GetPathInfo>
            //</response>


            ClientResponse r = _client.IMFS_GetInfo(GetSessionToken(), path.ToString());

            XmlReader rdr = r.AsXmlReader();


            while (rdr.Read())
            {

                if (rdr.NodeType == XmlNodeType.Element)
                {
                    if (rdr.Name == "Data")
                    {
                        string key = rdr.GetAttribute("Key");
                        string value = Utils.ReadString(rdr);

                        this.AddMetadata(rt, key, value);
                    }
                   
                }
              
            }
         






                // OLD (before getpathinfo)

//<?xml version="1.0" encoding="UTF-8" ?>
//<Response>
//   <ResponseCode>0</ResponseCode>
//   <Metadata>
//     <Type>BitRate</Type>
//       <Value>4444</Value>
//  </Metadata>
//</Response>



        
            //ClientResponse r = _client.Metadata_GetMetadata(GetSessionToken(), path.ToString());



            //XmlReader rdr = r.AsXmlReader();


          
            //string currentType = null;
            //string currentValue = null;
            //while (rdr.Read())
            //{

            //    if (rdr.NodeType == XmlNodeType.Element)
            //    {
            //        if (rdr.Name == "Type")
            //        {
            //            currentType = Utils.ReadString(rdr);
            //        }
            //        if (rdr.Name == "Value")
            //        {
            //            currentValue = Utils.ReadString(rdr);
            //        }
            //    }
            //    if (rdr.NodeType == XmlNodeType.EndElement)
            //    {
            //        if (rdr.Name == "Metadata")
            //        {
            //            this.AddMetadata(rt, currentType, currentValue);
            //            currentType = null;
            //            currentValue = null;
            //        }
            //    }
            //}



            // also include http headers on the actual file
            ClientResponse head = this.HeadFile(path);

            foreach (KeyValuePair<string, string> kvp in head.Headers)
            {
                string value = kvp.Value;
                if (kvp.Key == HttpHeader.ContentLength)
                {
                    value = this.ComputeSizeString(long.Parse(value));
                }

                this.AddMetadata(rt, kvp.Key, value);
            }



            return rt;


        }


 

        // transfer
        public void Transfer(IFileItem source, Path destination, Action<long> bytesTransferred)
        {
            this.Transfer(source, destination, null, bytesTransferred);
        }
        public void Transfer(IFileItem source, Path destination, NameValuePairs metadata, Action<long> bytesTransferred)
        {
            this.Upload(destination, MediaType.ApplicationOctetStream, source.Size, source.GetInputStream(), bytesTransferred);

            if (metadata != null)
            {
                this.SetMetadata(destination, metadata);
            }
        }


        public void SetMetadata(Path target, NameValuePairs metadata)
        {
            ClientResponse r = _client.Metadata_SetMetadata(GetSessionToken(), target.ToString(), metadata);

            object o = r;
        }














        private IEnumerable<IPathItem> BuildFolder(XmlElement element, Path path)
        {
            if (element.Name == "Folder")
            {
                string name = element.SelectSingleNode("Name").InnerText;
                yield return new FolderItem(this, path.Combine(name + Path.Separator), null);
            }

        }

        private IEnumerable<IPathItem> BuildFile(XmlElement element, Path path)
        {
            if (element.Name == "File")
            {
                string sizeBytes = element.SelectSingleNode("SizeBytes").InnerText;
                string name = element.SelectSingleNode("Name").InnerText;
                string createdDate = element.SelectSingleNode("CreatedDate").InnerText;
                yield return new FileItem(this, path.Combine(name), DateTime.Parse(createdDate), long.Parse(sizeBytes));
            }

        }

        private IEnumerable<XmlElement> ListFolder(Path folderPath)
        {

            //Response>
            //    <ResponseCode>0</ResponseCode>
            //    <ListFolder>
            //        <TotalFolderCount>1</TotalFolderCount>
            //        <TotalFileCount>0</TotalFileCount>
            //        <PageFolderCount>1</PageFolderCount>
            //        <PageFileCount>0</PageFileCount>
            //        <Folder>
            //            <FolderCount>0</FolderCount>
            //            <FileCount>0</FileCount>
            //            <Name>a</Name>
            //            <Path>working/temp633347224987215000/a</Path>
            //            <CreatedDate>Mon, 31 Dec 2007 23:29:38 GMT</CreatedDate>
            //            <Metadata />
            //            <FileTags />
            //        </Folder>
            //    </ListFolder>
            //</Response>

            //<File>
            //    <SizeBytes>1</SizeBytes>
            //    <Name>b</Name>
            //    <Path>working/temp633347290272371250/b</Path>
            //    <CreatedDate>Tue, 01 Jan 2008 01:18:30 GMT</CreatedDate>
            //    <Metadata />
            //    <FileTags />
            //</File>


            int pageSize = 100;

            int currentPage = 1;
            ClientResponse response;
            bool keepGoing;
            do
            {
                keepGoing = false;
                response = _client.IMFS_ListFolder(GetSessionToken(), folderPath.ToString(), currentPage++, pageSize, null, null, null, null);

                XmlDocument doc = response.AsXmlDocument();
                long totalFolderCount = long.Parse(doc.SelectSingleNode("//ListFolder/TotalFolderCount").InnerText);
                long totalFileCount = long.Parse(doc.SelectSingleNode("//ListFolder/TotalFileCount").InnerText);
                long pageFolderCount = long.Parse(doc.SelectSingleNode("//ListFolder/PageFolderCount").InnerText);
                long pageFileCount = long.Parse(doc.SelectSingleNode("//ListFolder/PageFileCount").InnerText);

                foreach (XmlElement element in doc.SelectNodes("//ListFolder/*"))
                {
                    if (!keepGoing && (pageFileCount < totalFileCount || pageFolderCount < totalFolderCount) && Utils.In(element.Name, "Folder", "File"))
                    {
                        keepGoing = true;
                    }
                    yield return element;
                }

            } while (keepGoing);
        }


        private void Upload(Path destination, string contents, Action<long> bytesIn)
        {
            this.Upload(destination, "text/plain", contents.Length, Utils.ToStream(contents), bytesIn);
        }

        private void Upload(Path destination, string contentType, long contentLength, Stream contentStream, Action<long> bytesIn)
        {
            ClientResponse response = _client.IMFS_GetStorageNode(GetSessionToken(), contentLength);

            //<Response>
            //   <ResponseCode>0</ResponseCode>
            //   <GetStorageNode>
            //      <UploadHost>123node1.nirvanix.com</UploadHost>
            //      <UploadToken>myuploadtoken</UploadToken>
            //   </GetStorageNode>
            //</Response>

            string uploadHost = response.AsXmlDocument().SelectSingleNode("//UploadHost").InnerText;
            string uploadToken = response.AsXmlDocument().SelectSingleNode("//UploadToken").InnerText;

            PostClient client = new PostClient(new Uri(_client.RootUri.Scheme + "://" + uploadHost + "/"));
            client.Tracer.AddListener(this.Tracer);

            NameValuePairs nvp = new NameValuePairs();
            nvp["uploadToken"] = uploadToken;
            nvp["destFolderPath"] = destination.Parent.ToString();

            ClientResponse response2 = client.PostRepresentation("Upload.ashx", destination.Name, contentType, contentLength, nvp, contentStream, bytesIn);


            //<?xml version="1.0" encoding="utf-8"?>
            //<Response>
            //    <ResponseCode>0</ResponseCode>
            //    <FilesUploaded>1</FilesUploaded>
            //    <BytesUploaded>1</BytesUploaded>
            //</Response>
        }



  





        private ClientResponse HeadFile(Path filePath)
        {
            ClientResponse rt = this.FollowDownloadNode(filePath, delegate(NonBufferingClient item0, string item1, NameValuePairs item2)
            {
                return item0.NonBufferedHead(item1, item2);
            });

            return rt;
        }



        private void Validate()
        {
            // TODO how to validate the appname?  this is very inefficient!

            // look for the first file, and try to generate a download link

            IFileItem firstFile = this.FindFirstFile(Path.Root);

            if (firstFile == null)
            {
                // assume it's valid...
                return;
            }

            // else check the head response
            ClientResponse rt = this.HeadFile(firstFile.Path);

            if (!rt.HttpStatus.Equals(HttpStatus.OK))
            {
                throw CodePlexException.Format("Invalid AppName '{0}'", _appName);
            }


        }

        private IFileItem FindFirstFile(Path folderPath)
        {

            foreach (IPathItem pi in this.GetItems(folderPath))
            {

                IFileItem fi = pi as IFileItem;
                if (fi == null)  // it's a folder
                {
                    fi = FindFirstFile(pi.Path);
                }

                if (fi != null)
                {
                    // found one!
                    return fi;
                }


            }
            return null;


        }

        
        private string GetSessionToken()
        {
            /// /ws/Authentication/Login.ashx?appKey=8da051b0-a60f-4c22-a8e0-d9380edafa6f&userName=testuser&password=Abc123

            //<?xml version="1.0" encoding="utf-8"?>
            //<Response>
            //   <ResponseCode>0</ResponseCode>
            //   <SessionToken>54592180-7060-4D4B-BC74-2566F4B2F943</SessionToken>
            //</Response>

            if (_sessionToken == null)
            {
                ClientResponse response = _client.Authentication_Login(_appKey, _username, _password);
                XmlDocument doc = response.AsXmlDocument();
                int responseCode = int.Parse(doc.SelectSingleNode("//ResponseCode").InnerText);
                if (responseCode != 0)
                {
                    string errorMessage = doc.SelectSingleNode("//ErrorMessage").InnerText;

                    throw CodePlexException.Format("Nirvanix Error {0}: '{1}'", responseCode, errorMessage);
                }

                _sessionToken =  doc.SelectSingleNode("//SessionToken").InnerText;

            }
            return _sessionToken;
        }

        private ClientResponse FollowDownloadNode(Path target, Fn<NonBufferingClient, string, NameValuePairs, ClientResponse> fn)
        {
            ClientResponse response = _client.IMFS_GetDownloadNodes(this.GetSessionToken(), target.ToString());

            //<Response>
            //   <ResponseCode>0</ResponseCode>
            //   <DownloadNode>node1.nirvanix.com</DownloadNode>
            //   <DownloadNode>node1.nirvanix.com</DownloadNode>
            //</Response>

            string downloadNode = response.AsXmlDocument().SelectSingleNode("//DownloadNode").InnerText;

            NonBufferingClient client = new NonBufferingClient(new Uri(_client.RootUri.Scheme + "://" + downloadNode + "/"));
            client.Tracer.AddListener(this.Tracer);

            NameValuePairs nvp = new NameValuePairs();
            nvp["sessionToken"] = this.GetSessionToken();

            string path = _appName + "/" + _username + "/" + Utils.UrlEncode(target.ToString().Substring(1));
            ClientResponse response2 = fn(client,path, nvp);
            return response2;


            //  http://123.123.123.123/MyApp/MyAccount/My%20Folder/My%20File%201.txt?sessionToken=8da051b0-a60f-4c22-a8e0-d9380edafa6f

        }













        private class NonBufferingClient : Client
        {
            public NonBufferingClient(Uri rootUri) : base(rootUri) { }


            public ClientResponse NonBufferedGet(string path, NameValuePairs parameters)
            {

                GetRequest request = GetRequest.Create(path, parameters);
                request.BufferOutput = false;
                return this.DoRequest(request);
               // PostRequest request = null;
                
              //  return this.DoRequest(HttpMethod.Get, Utils.EncodeUrlPath(path, parameters), new NameValuePairs(), null, null, null, false);
            }

            public ClientResponse NonBufferedHead(string path, NameValuePairs parameters)
            {
                HeadRequest request = HeadRequest.Create(path, parameters);
                request.BufferOutput = false;
                return this.DoRequest(request);
                //return this.DoRequest(HttpMethod.Head, Utils.EncodeUrlPath(path, parameters), new NameValuePairs(), null, null, null, false);
            }

        }

        private class PostClient : Client
        {
            public PostClient(Uri rootUri) : base(rootUri) { }

            public ClientResponse PostRepresentation(string path, string filename, string contentType, long contentLength, NameValuePairs parameters, Stream input, Action<long> bytesIn)
            {
                string boundary = "BbC04y";

                // TODO streamify, for now just use a buffer
                MemoryStream buffer = new MemoryStream();
                StreamWriter w = new StreamWriter(buffer);
                w.WriteLine("--" + boundary);
                w.WriteLine(@"Content-Disposition: file; filename=""" + filename + @"""");
                w.WriteLine("Content-Type: " + contentType);
                w.WriteLine("Content-Transfer-Encoding: binary");
                w.WriteLine();
                w.Flush();
                Utils.BufferedTransfer(input, buffer);

                StreamWriter w2 = new StreamWriter(buffer);
                w2.WriteLine();
                w2.WriteLine("--" + boundary + @"--");
                w2.Flush();
                buffer.Position = 0;

                NameValuePairs headers = new NameValuePairs();
                headers[HttpHeader.ContentType] = MediaType.MultipartFormData + "; boundary=" + boundary;
                headers[HttpHeader.ContentLength] = buffer.Length.ToString();

                PostRequest request = PostRequest.Create(path, parameters, headers, buffer);
                request.BytesIn = bytesIn;
                return this.DoRequest(request);
                //return this.DoRequest(HttpMethod.Post, Utils.EncodeUrlPath(path, parameters), headers, buffer, bytesIn, null, true);
            }


        }



        private class FileItem : PathItem, IFileItem, ISupportsQuickMD5
        {
           
            private readonly long _size;

            public FileItem(IFileSystem fileSystem, Path path, DateTime? lastModified, long size)
                : base(fileSystem, path, lastModified) 
            {
                _size = size;
            }


            public long Size
            {
                get { return _size; }
            }

            public byte[] QuickMD5
            {
                get 
                {
                    NirvanixFileSystem nfs = this.FileSystem as NirvanixFileSystem;

                    Metadata md = nfs.GetMetadata(this.Path);
                    string base64 = md["MD5"];
                    return Convert.FromBase64String(base64);

                }
            }

            public Stream GetInputStream()
            {
                NirvanixFileSystem nfs = this.FileSystem as NirvanixFileSystem;

                ClientResponse rt = nfs.FollowDownloadNode(this.Path, delegate(NonBufferingClient item0, string item1, NameValuePairs item2)
                {
                    return item0.NonBufferedGet(item1, item2);
                });

                return rt.AsStream();

            }

        }


        
    }
}
